#!/usr/bin/env python2
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import atexit
import errno
import fcntl
import os
import pwd
import shlex
import shutil
import socket
import subprocess
import sys
import tempfile

base = os.path.dirname(os.path.abspath(__file__))


def abspath(x):
    return os.path.abspath(os.path.join(base, x))


def usage():
    raise SystemExit(
        ('Usage: %s 32|64'
         ' [the rest of the command line is passed to main.py]') % sys.argv[0])


arch = sys.argv[1].decode('utf-8')
if arch == 'shutdown':
    raise SystemExit(0)
if arch not in '64 32'.split() or len(sys.argv) < 2:
    usage()


def mkdir(x):
    try:
        os.mkdir(abspath(x))
    except EnvironmentError as err:
        if err.errno == errno.EEXIST:
            return
        raise
    if 'SUDO_UID' in os.environ and os.geteuid() == 0:
        os.chown(
            abspath(x),
            int(os.environ['SUDO_UID']), int(os.environ['SUDO_GID']))


mkdir('sources-cache')
mkdir('build')
mkdir('build/linux')
output_dir = os.path.join(abspath('build'), 'linux', arch)
mkdir(output_dir)
img_path = os.path.abspath(
    os.path.realpath(os.path.join(output_dir, 'chroot-1')))
sw_dir = os.path.join(output_dir, 'sw')
mkdir(sw_dir)


def print_cmd(cmd):
    if not print_cmd.silent_calls:
        print('\033[92m', end='')
        print(*cmd, end='\033[0m\n')


print_cmd.silent_calls = False


def call(*cmd):
    if len(cmd) == 1 and isinstance(cmd[0], basestring):
        cmd = shlex.split(cmd[0])
    print_cmd(cmd)
    ret = subprocess.Popen(cmd).wait()
    if ret != 0:
        raise SystemExit(ret)


def check_for_image(tag):
    return os.path.exists(img_path)


def cached_download(url):
    bn = os.path.basename(url)
    local = os.path.join('/tmp', bn)
    if not os.path.exists(local):
        from urllib import urlopen
        print('Downloading', url, '...')
        data = urlopen(url).read()
        with open(local, 'wb') as f:
            f.write(data)
    return local


def copy_terminfo():
    raw = subprocess.check_output(['infocmp']).decode('utf-8').splitlines()[0]
    path = raw.partition(':')[2].strip()
    if path and os.path.exists(path):
        bdir = os.path.basename(os.path.dirname(path))
        dest = os.path.join(img_path, 'usr/share/terminfo', bdir)
        call('sudo', 'mkdir', '-p', dest)
        call('sudo', 'cp', '-a', path, dest)


def chroot(cmd, as_root=True):
    if isinstance(cmd, basestring):
        cmd = shlex.split(cmd)
    print_cmd(['in-chroot'] + cmd)
    print_cmd.silent_calls = True
    user = pwd.getpwuid(os.geteuid()).pw_name
    env = {
        'PATH': '/sbin:/usr/sbin:/bin:/usr/bin',
        'PS1': '\x1b[92mchroot\x1b[0m ({}-bit) {}'.format(arch, '#' if as_root else '$'),
        'HOME': '/root' if as_root else '/home/' + user,
        'USER': 'root' if as_root else user,
        'TERM': os.environ.get('TERM', 'xterm-256color'),
    }
    us = [] if as_root else ['--userspec={}:{}'.format(os.geteuid(), os.getegid())]
    as_arch = ['linux{}'.format(arch), '--']
    cmd = ['sudo', 'chroot'] + us + [img_path] + as_arch + list(cmd)
    copy_terminfo()
    call('sudo', 'cp', '/etc/resolv.conf', os.path.join(img_path, 'etc'))
    print_cmd.silent_calls = False
    ret = subprocess.Popen(cmd, env=env).wait()
    if ret != 0:
        raise SystemExit(ret)


def write_in_chroot(path, data):
    path = path.lstrip('/')
    p = subprocess.Popen(['sudo', 'tee', os.path.join(img_path, path)], stdin=subprocess.PIPE, stdout=open(os.devnull, 'wb'))
    if not isinstance(data, bytes):
        data = data.encode('utf-8')
    p.communicate(data)
    if p.wait() != 0:
        raise SystemExit(p.returncode)


def _build_container():
    url = 'https://partner-images.canonical.com/core/unsupported/precise/current/ubuntu-precise-core-cloudimg-{}-root.tar.gz'
    archive = cached_download(url.format('amd64' if arch == '64' else 'i386'))
    user = pwd.getpwuid(os.geteuid()).pw_name
    if os.path.exists(img_path):
        call('sudo', 'rm', '-rf', img_path)
    mkdir(img_path)
    call('sudo tar -C "{}" -xpf "{}"'.format(img_path, archive))
    if os.getegid() != 100:
        chroot('groupadd -f -g {} {}'.format(os.getegid(), 'crusers'))
    chroot('useradd --home-dir=/home/{user} --create-home --uid={uid} --gid={gid} {user}'.format(
        user=user, uid=os.geteuid(), gid=os.getegid()))
    # Prevent services from starting
    write_in_chroot('/usr/sbin/policy-rc.d', '#!/bin/sh\nexit 101')
    chroot('chmod +x /usr/sbin/policy-rc.d')
    # prevent upstart scripts from running during install/update
    chroot('dpkg-divert --local --rename --add /sbin/initctl')
    chroot('cp -a /usr/sbin/policy-rc.d /sbin/initctl')
    chroot('''sed -i 's/^exit.*/exit 0/' /sbin/initctl''')
    # remove apt-cache translations for fast "apt-get update"
    write_in_chroot('/etc/apt/apt.conf.d/chroot-no-languages', 'Acquire::Languages "none";')

    for cmd in [
        # Basic build environment
        'apt-get update',
        'apt-get install -y build-essential nasm python-argparse cmake chrpath zsh git',
        # Build time deps for Qt. See http://doc.qt.io/qt-5/linux-requirements.html and https://wiki.qt.io/Building_Qt_5_from_Git
        'apt-get install -y flex bison gperf ruby libx11-dev libxext-dev libxfixes-dev'
        ' libxi-dev libxrender-dev libxcb1-dev libx11-xcb-dev libxcb-glx0-dev xkb-data libglu1-mesa-dev libgtk2.0-dev',
        # Cleanup
        'apt-get clean',
        'chsh -s /bin/zsh ' + user,
    ]:
        chroot(cmd)
    call('sudo chown {}:{} "{}"'.format(os.getuid(), os.getgid(), img_path))


def build_container():
    try:
        _build_container()
    except Exception:
        failed_img_path = os.path.join(os.path.dirname(img_path), 'failed')
        if os.path.exists(failed_img_path):
            call('sudo rm -rf "{}"'.format(failed_img_path))
        call('sudo mv "{}" "{}"'.format(img_path, failed_img_path))
        call('sudo chown {}:{} "{}"'.format(os.getuid(), os.getgid(), failed_img_path))
        raise


def get_mounts():
    ans = {}
    with open('/proc/self/mountinfo') as f:
        for line in f:
            line = line.decode('utf-8')
            parts = line.split()
            src, dest = parts[3:5]
            ans[os.path.abspath(os.path.realpath(dest))] = src
    return ans


def mount_all(tdir):
    print_cmd.silent_calls = True

    current_mounts = get_mounts()

    def mount(src, dest, readonly=False):
        dest = os.path.join(img_path, dest.lstrip('/'))
        if dest not in current_mounts:
            call('sudo', 'mkdir', '-p', dest)
            call('sudo', 'mount', '--bind', src, dest)
            if readonly:
                call('sudo', 'mount', '-o', 'remount,ro,bind', dest)

    mount(tdir, '/tmp')
    mount(sw_dir, '/sw')
    mount('sources-cache', '/sources')
    mount('scripts', '/scripts', True)
    mount('patches', '/patches', True)
    calibre_dir = os.environ.get('CALIBRE_SRC_DIR',
                                 os.path.join('..', 'calibre'))
    if os.path.exists(os.path.join(calibre_dir, 'setup.py')):
        mount(calibre_dir, '/calibre', True)
    mount('/dev', '/dev')
    call('sudo', 'mount', '-t', 'proc', 'proc', os.path.join(img_path, 'proc'))
    call('sudo', 'mount', '-t', 'sysfs', 'sys', os.path.join(img_path, 'sys'))
    call('sudo', 'chmod', 'a+w', os.path.join(img_path, 'dev/shm'))
    call('sudo', 'mount', '--bind', '/dev/shm', os.path.join(img_path, 'dev/shm'))
    print_cmd.silent_calls = False


def umount_all():
    print_cmd.silent_calls = True
    for mp in sorted(get_mounts(), key=len, reverse=True):
        if mp.startswith(img_path):
            call('sudo', 'umount', '-l', mp)
    print_cmd.silent_calls = False


def shutdown():
    pass


def eintr_retry_call(func, *args, **kwargs):
    while True:
        try:
            return func(*args, **kwargs)
        except EnvironmentError as e:
            if getattr(e, 'errno', None) == errno.EINTR:
                continue
            raise


def singleinstance():
    name = 'build-calibre-%s-singleinstance' % arch
    if not isinstance(name, bytes):
        name = name.encode('utf-8')
    address = b'\0' + name.replace(b' ', b'_')
    sock = socket.socket(family=socket.AF_UNIX)
    try:
        eintr_retry_call(sock.bind, address)
    except socket.error as err:
        if getattr(err, 'errno', None) == errno.EADDRINUSE:
            return False
        raise
    fd = sock.fileno()
    old_flags = fcntl.fcntl(fd, fcntl.F_GETFD)
    fcntl.fcntl(fd, fcntl.F_SETFD, old_flags | fcntl.FD_CLOEXEC)
    atexit.register(sock.close)
    return True


if not singleinstance():
    raise SystemExit('Another instance of the linux container is running')

if sys.argv[2:] == ['container']:
    build_container()
    raise SystemExit(0)

if sys.argv[1:] == ['shutdown']:
    shutdown()
    raise SystemExit(0)

if not check_for_image(arch):
    build_container()


def run():
    print_cmd.silent_calls = True
    tdir = tempfile.mkdtemp()
    zshrc = os.path.realpath(os.path.expanduser('~/.zshrc'))
    if os.path.exists(zshrc):
        shutil.copy2(zshrc, os.path.join(tdir, '.zshrc'))
    else:
        open(os.path.join(tdir, '.zshrc'), 'wb').close()
    try:
        mount_all(tdir)
        cmd = [
            'python2', '/scripts/main.py'
        ] + sys.argv[2:]
        os.environ.pop('LANG', None)
        for k in tuple(os.environ):
            if k.startswith('LC') or k.startswith('XAUTH'):
                del os.environ[k]
        chroot(cmd, as_root=False)
    finally:
        umount_all()
        shutil.rmtree(tdir)


run()
